﻿using TvdP;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Linq.Expressions;
using System.Collections.Specialized;

namespace TvdP
{
    public static partial class Behaviors
    {

        internal sealed class Composer : IBehaviors, IResetable
        {
            public IEnumerable<Delegate> _BehaviorCollection;
            public IAdapters _AdapterStore;

            ConditionalWeakTable<Type, Action<object, object>> _ApplicableBehaviorsCache =
                new ConditionalWeakTable<Type, Action<object, object>>();

            Action<object, object> GetApplicableBehaviorInitializersInvoker(Type type)
            {
                return _ApplicableBehaviorsCache.GetValue(type, BuildApplicableBehaviorInitializersInvoker);
            }

            /// <summary>
            /// Gets types of the <paramref name="behaviorInitializer"/> delegate parameters, except the last parameter
            /// which is the context and of type object.
            /// </summary>
            /// <param name="behaviorInitializer"></param>
            /// <returns></returns>
            static IEnumerable<Type> GetParameterTypes(Delegate behaviorInitializer)
            {
                var prms = behaviorInitializer.Method.GetParameters();

                return prms.Take(prms.Length - 1).Select(prm => prm.ParameterType);
            }

            /// <summary>
            /// Build an action delegate that takes an object to be extended and a context and
            /// invokes the initializers of all applicable behaviors for that object.
            /// The true type of the object in that case must be of the given <paramref name="type"/>.
            /// </summary>
            /// <param name="type"></param>
            /// <returns></returns>
            Action<object, object> BuildApplicableBehaviorInitializersInvoker(Type type)
            {
                //build invoker of form:
                //Action<object,object> invoker = (tgt, ctxt) =>
                //    {
                //        A a = AAdapter(tgt);
                //        B b = BAdapter(tgt);
                //        C c = CAdapter(tgt);
                //        ...

                //        Behavior1(a, ctxt);
                //        Behavior2(a, b, ctxt);
                //        Behavior3(c, ctxt);
                //        ...
                //    }


                var applicableBehaviorInitializers =
                    _BehaviorCollection
                        .Where(
                            behavior => GetParameterTypes(behavior).All(t => _AdapterStore.CanCast(type, t))
                        )
                        .Select(initializer => Behavior.VerifyBehavior(initializer, "Initializer"))
                        .ToArray()
                ;

                if (applicableBehaviorInitializers.Length == 0)
                    return (a, b) => { };

                //collect all distinct types our target needs to be adapted to so input can be provided
                //for all behavior initializers.
                var initializerParameterTypes =
                    applicableBehaviorInitializers
                        .SelectMany(behaviorInitializer => GetParameterTypes(behaviorInitializer))
                        .Distinct() //no duplicates
                        .ToArray()
                ;

                //get only those initializer parameter types that can not be assigned to from any other adapt-to-type (masters)
                var masterInitializerParameterTypes =
                    initializerParameterTypes
                        .Where(t => !initializerParameterTypes.Any(tMaster => tMaster != t && t.IsAssignableFrom(tMaster)))
                ;

                var adapteeArgParameterExpression = Expression.Parameter(typeof(object));

                var masterInitializerParameterVariables =
                    masterInitializerParameterTypes
                    //create variables and initialization expressions for the adapt-to-types, but only those that are not assignable from other adapt-to-types.
                        .Select(
                            parameterType =>
                            {
                                var variableExpression = Expression.Variable(parameterType);

                                return
                                    new
                                    {
                                        variableType = parameterType,
                                        variableExpression = variableExpression,
                                        variableInitializationExpression = (Expression)
                                            Expression.Assign(
                                                variableExpression,
                                                Expression.Call(
                                                    null,
                                                    typeof(Adapters).GetMethod("Cast").MakeGenericMethod(parameterType),
                                                    Expression.Constant(_AdapterStore),
                                                    adapteeArgParameterExpression
                                                )
                                            )
                                    }
                                ;
                            }
                        )
                        .ToArray()
                ;

                //map individual adapt-to-types to variables. The variable will be of a type that can be assigned to the desired adapt-to-type.
                var initializerParameterTypeToVariableExpressionMap =
                    initializerParameterTypes.ToDictionary(
                        t => t,
                        t =>
                            (Expression)masterInitializerParameterVariables
                                .First(a => t.IsAssignableFrom(a.variableType))
                                .variableExpression
                    )
                ;

                var ctxtArgParameterExpression = Expression.Parameter(typeof(object));

                //Create call expressions to all behavior initializers.
                var behaviorInitializerCallExpressions =
                    applicableBehaviorInitializers
                        .Select(
                            behaviorInitializer =>
                            {
                                var parameterTypes = GetParameterTypes(behaviorInitializer);

                                return
                                    Expression.Call(
                                        behaviorInitializer.Target == null ? null : Expression.Constant(behaviorInitializer.Target),
                                        behaviorInitializer.Method,
                                        Enumerable.Concat(
                                            parameterTypes.Select(t => initializerParameterTypeToVariableExpressionMap[t]),
                                            new Expression[] { ctxtArgParameterExpression }
                                        )
                                            .ToArray()
                                    )
                                ;
                            }
                        )
                ;

                var behaviorInitializersInvoker =
                    Expression.Lambda<Action<object, object>>(
                        Expression.Block(
                            masterInitializerParameterVariables.Select(a => a.variableExpression),
                            Enumerable.Concat(
                    //initilize variables via adapters
                                masterInitializerParameterVariables.Select(a => a.variableInitializationExpression),
                    //call behavior initializers
                                behaviorInitializerCallExpressions
                            )
                        ),
                        adapteeArgParameterExpression,
                        ctxtArgParameterExpression
                    )
                    .Compile();

                return behaviorInitializersInvoker;
            }

            void IBehaviors.Apply(object target, object context)
            {
                if (target == null)
                    throw new ArgumentNullException("target");

                object tgtBase = _AdapterStore.GetBase(target);

                GetApplicableBehaviorInitializersInvoker(tgtBase.GetType())(tgtBase, context);
            }

            public void Reset()
            { _ApplicableBehaviorsCache = new ConditionalWeakTable<Type, Action<object, object>>(); }
        }

        /// <summary>
        /// Create an IBehaviors object using the given <paramref name="behaviorCollection"/> and <paramref name="adapters"/> object.
        /// </summary>
        /// <remarks>
        /// The resulting IBehaiors object will also implement IResetable. The Reset() method should be called whenever the contents of <paramref name="behaviorCollection"/> change
        /// or it gets known that <paramref name="adapters"/> may support different adaptations.
        /// </remarks>
        /// <param name="behaviorCollection"></param>
        /// <param name="adapters"></param>
        /// <returns></returns>
        public static IBehaviors CreateComposed(IEnumerable<Delegate> behaviorCollection, IAdapters adapters)
        {
            if (behaviorCollection == null)
                throw new ArgumentNullException("behaviorCollection");

            if (adapters == null)
                throw new ArgumentNullException("adapterStore");

            return
                new Composer
                {
                    _AdapterStore = adapters,
                    _BehaviorCollection = behaviorCollection
                }
            ;
        }
    }
}
